import time
import pandas as pd
import requests
import re
import numpy as np
import os
import glob
import seaborn as sns
from pandas import DataFrame
from datetime import datetime, date
from bs4 import BeautifulSoup as bs
from os.path import basename
from sklearn.linear_model import LinearRegression
from matplotlib import pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from sklearn.preprocessing import StandardScaler
Les adresses WEB des liens de téléchargement pour le classement ressemblent toutes à l'adresse suivante, à la date près : https://www.vendeeglobe.org/download-race-data/vendeeglobe_20210305_080000.xlsx
#generation de la liste de toutes les dates des classements
liste_des_heures = [4,8,11,14,17,21]
#seulement les dates pour lesquelles on a toutes les heures
liste_des_dates = pd.date_range(datetime(2020,11,9), datetime(2021,3,4))
#on ajoute à la main les premières dates
dates_classements = ["20201108_140000", "20201108_150000",
"20201108_170000", "20201108_210000"]
for date in liste_des_dates:
for heure in liste_des_heures:
date_heure = date.replace(hour = heure)
date_heure = date_heure.strftime("%Y%m%d_%H%M%S")
dates_classements.append(date_heure)
dates_classements = dates_classements+["20210305_040000","20210305_080000"]
#on ajoute à la main les dernières dates
#dates_classements
dates_df = pd.DataFrame(dates_classements)
def chargement_classements():
for date in dates_classements:
url = "https://www.vendeeglobe.org/download-race-data/vendeeglobe_"+date+".xlsx"
dl = requests.get(url)
with open(date+".xlsx", 'wb') as file:
file.write(dl.content)
file.close()
#chargement_classements() #Données déjà téléchargées - ne pas exécuter
glossaire = requests.get("https://www.vendeeglobe.org/fr/glossaire")
content = glossaire.content.decode('utf-8')
soup = bs(content)
#soup
On constate en analysant le code source que les données techniques sur les voiliers sont stockées dans la classe "boats-list__popup-specs-list".
voiliers = soup.find_all('ul', {'class': "boats-list__popup-specs-list"})
skippers = soup.find_all('span', {'class': "boats-list__skipper-name"})
#On vérifie qu'on a bien les 33 voiliers et skippers du site
print(len(voiliers),len(skippers))
33 33
#On regarde le nombre (maximal) de caractéristiques données pour chaque voilier
nb_col = []
for i in range(len(voiliers)):
nb_col.append(len(voiliers[i].find_all('li')))
print(nb_col)
print("""On a au minimum %s caractéristiques, et au maximum (et fréquemment) %s caractéristiques,
et c'est le cas pour le voilier n°%s.
On choisit donc ce voilier comme référence, pour définir les colonnes à utiliser par la suite."""
%(min(nb_col),max(nb_col),nb_col.index(max(nb_col))))
[14, 14, 14, 14, 13, 14, 14, 14, 14, 14, 13, 14, 14, 14, 13, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 12, 14, 13, 13, 14, 13, 13, 12] On a au minimum 12 caractéristiques, et au maximum (et fréquemment) 14 caractéristiques, et c'est le cas pour le voilier n°0. On choisit donc ce voilier comme référence, pour définir les colonnes à utiliser par la suite.
#On extrait la liste des caractéristiques des voiliers
col = []
for li_tag in voiliers[0].find_all('li'):
col.append(li_tag.text.split(sep=':')[0][:-1])
col
['Numéro de voile', 'Anciens noms du bateau', 'Architecte', 'Chantier', 'Date de lancement', 'Longueur', 'Largeur', "Tirant d'eau", 'Déplacement (poids)', 'Nombre de dérives', 'Hauteur mât', 'Voile quille', 'Surface de voiles au près', 'Surface de voiles au portant']
df_voiliers = pd.DataFrame(columns=["Skipper"]+col)
dico={}
for i in range(len(voiliers)):
dico = {li_tag.text.split(sep=':')[0][:-1]:li_tag.text.split(sep=':')[1][1:]
for li_tag in voiliers[i].find_all('li')}
dico["Skipper"] = skippers[i].text
df_voiliers = df_voiliers.append(pd.DataFrame(dico,index=[i]))
df_voiliers.head(5)
| Skipper | Numéro de voile | Anciens noms du bateau | Architecte | Chantier | Date de lancement | Longueur | Largeur | Tirant d'eau | Déplacement (poids) | Nombre de dérives | Hauteur mât | Voile quille | Surface de voiles au près | Surface de voiles au portant | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | Fabrice AMEDEO | FRA 56 | No Way Back, Vento di Sardegna | VPLP/Verdier | Persico Marine | 01 Août 2015 | 18,28 m | 5,85 m | 4,50 m | 7 t | foils | 29 m | monotype | 320 m2 | 570 m2 |
| 1 | Romain ATTANASIO | FRA 49 | Gitana Eighty, Synerciel, Newrest-Matmut | Bruce Farr Design | Southern Ocean Marine (Nouvelle Zélande) | 08 Mars 2007 | 18,28m | 5,80m | 4,50m | 9t | 2 | 28m | acier forgé | 280 m2 | 560 m2 |
| 2 | Alexia BARRIER | FRA72 | Famille Mary-Etamine du Lys, Initiatives Coeur... | Marc Lombard | MAG France | 01 Mars 1998 | 18,28m | 5,54m | 4,50m | 9t | 2 | 29 m | acier | 260 m2 | 580 m2 |
| 3 | Yannick BESTAVEN | 17 | Safran 2 - Des Voiles et Vous | Verdier - VPLP | CDK Technologies | 12 Mars 2015 | 18,28 m | 5,80 m | 4,50 m | 8 t | foils | 29 m | acier mécano soudé | 310 m2 | 550 m2 |
| 4 | Jérémie BEYOU | 08 | NaN | VPLP | CDK Technologies | 18 Août 2018 | 18,28 m | 5,85 m | 4,50 m | 8t | foils | 29 m | acier | 320 m2 | 600 m2 |
#Enregistrement du df
#df_voiliers.to_csv('voiliers.csv', index=False)
#hors connexion
#df_voiliers = pd.read_csv('voiliers.csv')
#On extrait la liste de tous les fichiers de classement
files_classements = glob.glob('classements\\*') #tous fichiers
#Indice du premier fichier mentionnant l'arrivée du 1er skipper
files_classements.index("classements\\20210127_170000.xlsx")
482
#Liste des fichiers des classements avant l'arrivée du 1er skipper
files_classements_1 = files_classements[0:482]
#On définit les noms des colonnes à appliquer pour tous
col_class = ['Date','Rang', 'Nat. / Voile','Skipper / Bateau', 'Heure FR', 'Latitude',
'Longitude', 'Cap30', 'Vitesse30', 'VMG30', 'Distance30', 'CapLast', 'VitesseLast','VMGLast',
'DistanceLast', 'Cap24', 'Vitesse24','VMG24', 'Distance24', "DTF", "DTL"]
#Phase initiale - une fois la concaténation faite, plutôt récupérer les fichier csv
"""df_class_global = pd.read_excel(files_classements[0],skiprows=[0,1,2,3])
df_class_global.columns = col_class
df_class_global['Date'] = re.search('(2.*).xlsx', files_classements[0]).group(1)
df_class_global
for classement in files_classements[1:]:
df = pd.read_excel(classement,skiprows=[0,1,2,3])
df.columns = col_class
df['Date'] = re.search('(2.*).xlsx', classement).group(1)
df_class_global = df_class_global.append(df,ignore_index=True)
#df_class_global
df_class = pd.read_excel(files_classements_1[0],skiprows=[0,1,2,3,38,39,40,41])
df_class.columns = col_class
df_class['Date'] = re.search('(2.*).xlsx', files_classements_1[0]).group(1)
df_class
for classement in files_classements_1[1:]:
df = pd.read_excel(classement,skiprows=[0,1,2,3,38,39,40,41])
df.columns = col_class
df['Date'] = re.search('(2.*).xlsx', classement).group(1)
df_class = df_class.append(df,ignore_index=True)
#Enregistrement des df - pour ne pas refaire toute la concaténation à chaque fois
df_class_global.to_csv('df_class_global_concat.csv', index=False)
df_class.to_csv('df_class_concat.csv', index=False)
"""
df_class = pd.read_csv('df_class_concat.csv')
df_class_global = pd.read_csv('df_class_global_concat.csv')
#On retire les unités quand nécessaire (vitesse, cap, distance) et on passe en type float
df_class = df_class.replace({r'() kts': '',
r'()°$': '',
r'() nm': ''},
regex=True).astype(float, errors='ignore')
#on simplifie les heures en enlevant les "FR" et précisions sur les minutes à négliger
df_class["Heure FR"] = df_class["Heure FR"].str[0:5]
#on sépare les colonnes contenant deux types d'information avec passage à la ligne
df_class[["Skipper","Bateau"]] = df_class["Skipper / Bateau"].str.rsplit(pat="\n",expand=True)
df_class[["Nat","Voile"]] = df_class["Nat. / Voile"].str.rsplit(pat="\n",expand=True)
df_class = df_class.drop(columns=["Nat. / Voile", "Skipper / Bateau"])
#On met les date au format datetime, puis on sépare les jours et les heures
df_class["Date"] = pd.to_datetime(df_class["Date"], format="%Y%m%d_%H%M%S")
df_class["Heure"] = df_class["Date"].apply(lambda x: datetime.time(x))
df_class["Date"] = df_class["Date"].apply(lambda x: datetime.date(x))
#On réarrange l'ordre des colonnes
df_class = df_class[['Date', "Heure", 'Rang', 'Nat', 'Voile', 'Skipper',
'Bateau', 'Heure FR', 'Latitude', 'Longitude',
'Cap30', 'Vitesse30', 'VMG30','Distance30',
'CapLast', 'VitesseLast', 'VMGLast', 'DistanceLast',
'Cap24', 'Vitesse24', 'VMG24', 'Distance24', 'DTF', 'DTL']]
#On essaie de passer le Rang au format "int", mais cela ne fonctionne pas
df_class["Rang"].unique()
array(['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12',
'13', '14', '15', '16', '17', '18', '19', '20', '21', '22', '23',
'24', '25', '26', '27', '28', '29', '30', '31', 'NL', '32', '33',
'RET'], dtype=object)
#On regarde à quoi ressemblent les lignes avec des valeurs irrégulières pour "Rang"
df_class.loc[df_class["Rang"] == "NL"]
| Date | Heure | Rang | Nat | Voile | Skipper | Bateau | Heure FR | Latitude | Longitude | ... | CapLast | VitesseLast | VMGLast | DistanceLast | Cap24 | Vitesse24 | VMG24 | Distance24 | DTF | DTL | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 31 | 2020-11-08 | 14:00:00 | NL | FRA 79 | Charlie Dalin | APIVIA | NaN | NaN | NaN | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | |
| 32 | 2020-11-08 | 14:00:00 | NL | FRA 14 | Arnaud Boissieres | La Mie Câline - Artisans Artipôle | NaN | NaN | NaN | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | |
| 65 | 2020-11-08 | 15:00:00 | NL | FRA 14 | Arnaud Boissieres | La Mie Câline - Artisans Artipôle | NaN | NaN | NaN | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | |
| 98 | 2020-11-08 | 17:00:00 | NL | FRA 14 | Arnaud Boissieres | La Mie Câline - Artisans Artipôle | NaN | NaN | NaN | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | |
| 4552 | 2020-12-01 | 08:00:00 | NL | FRA 85 | Kevin Escoffier | PRB | NaN | NaN | NaN | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
5 rows × 24 columns
print(len(df_class.loc[df_class["Rang"] == "RET"]))
df_class.loc[df_class["Rang"] == "RET"].isna().sum()
2198
Date 0 Heure 0 Rang 0 Nat 0 Voile 0 Skipper 0 Bateau 0 Heure FR 2198 Latitude 2198 Longitude 2198 Cap30 2198 Vitesse30 2198 VMG30 2198 Distance30 2198 CapLast 2198 VitesseLast 2198 VMGLast 2198 DistanceLast 2198 Cap24 2198 Vitesse24 2198 VMG24 2198 Distance24 2198 DTF 2198 DTL 2198 dtype: int64
On constate que ces lignes sont toutes essentiellement vides (toutes les valeurs variables (vitesse, cap etc.) sont manquantes), et qu'elles peuvent donc être supprimées.
#liste de lignes à supprimer
ll = list(df_class.loc[df_class["Rang"] == "NL"].index) + list(df_class.loc[df_class["Rang"] == "RET"].index)
df_class = df_class.drop(ll)
df_class["Rang"].unique()
array(['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12',
'13', '14', '15', '16', '17', '18', '19', '20', '21', '22', '23',
'24', '25', '26', '27', '28', '29', '30', '31', '32', '33'],
dtype=object)
#On peut alors enfin passer le rang en "int"
df_class["Rang"] = df_class["Rang"].astype(int)
df_class["Rang"].unique()
array([ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17,
18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33])
#On procède de même pour le tableau global
#On retire les unités quand nécessaire (vitesse, cap, distance) et on passe en type float
df_class_global = df_class_global.replace({r'() kts': '',
r'()°$': '',
r'() nm': ''},
regex=True).astype(float, errors='ignore')
#on simplifie les heures en enlevant les "FR" et précisions sur les minutes à négliger
df_class_global["Heure FR"] = df_class_global["Heure FR"].str[0:5]
#on sépare les colonnes contenant deux types d'information avec passage à la ligne
df_class_global[["Skipper","Bateau"]] = df_class_global["Skipper / Bateau"].str.rsplit(pat="\n",expand=True)
df_class_global[["Nat","Voile"]] = df_class_global["Nat. / Voile"].str.rsplit(pat="\n",expand=True)
df_class_global = df_class_global.drop(columns=["Nat. / Voile", "Skipper / Bateau"])
#On met les date au format datetime, puis on sépare les jours et les heures
df_class_global["Date"] = pd.to_datetime(df_class_global["Date"], format="%Y%m%d_%H%M%S")
df_class_global["Heure"] = df_class_global["Date"].apply(lambda x: datetime.time(x))
df_class_global["Date"] = df_class_global["Date"].apply(lambda x: datetime.date(x))
#On réarrange l'ordre des colonnes
df_class_global = df_class_global[['Date', 'Heure', 'Rang', 'Nat', 'Voile', 'Skipper',
'Bateau', 'Heure FR', 'Latitude', 'Longitude',
'Cap30', 'Vitesse30', 'VMG30','Distance30',
'CapLast', 'VitesseLast', 'VMGLast', 'DistanceLast',
'Cap24', 'Vitesse24', 'VMG24', 'Distance24', 'DTF', 'DTL']]
#On impose que chaque ligne contienne au minimum 5 valeurs non NaN
df_class_global = df_class_global.dropna(thresh=5)
#On retire toutes les lignes d'en-tête
df_class_global = df_class_global.drop(df_class_global.loc[df_class_global["Heure FR"] == "Heure"].index)
#On nettoie les valeurs de la colonne "Rang"
df_class_global["Rang"].unique()
array(['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12',
'13', '14', '15', '16', '17', '18', '19', '20', '21', '22', '23',
'24', '25', '26', '27', '28', '29', '30', '31', 'NL', '32', '33',
'RET', '1\nARV', 'Rang\nRank', '2\nARV', '3\nARV', '4\nARV',
'5\nARV', '6\nARV', '7\nARV', '8\nARV', '9\nARV', '10\nARV',
'11\nARV', '12\nARV', '13\nARV', '14\nARV', '15\nARV', '16\nARV',
'17\nARV', '18\nARV', '19\nARV', '20\nARV', '21\nARV', '22\nARV',
'23\nARV', '24\nARV', '25\nARV'], dtype=object)
#liste d'autres lignes à supprimer
ll = []
ll += list(df_class_global.loc[df_class_global["Rang"] == "NL"].index.values)
ll += list(df_class_global.loc[df_class_global["Rang"] == "RET"].index.values)
ll += list(df_class_global.loc[df_class_global["Rang"].str.contains("Rang")].index.values)
df_class_global = df_class_global.drop(ll)
df_class_global["Rang"].unique()
#On conserve les mentions "\nARV" pour distinguer les classements finaux ou intermédiaires
#quitte à les supprimer plus tard
array(['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12',
'13', '14', '15', '16', '17', '18', '19', '20', '21', '22', '23',
'24', '25', '26', '27', '28', '29', '30', '31', '32', '33',
'1\nARV', '2\nARV', '3\nARV', '4\nARV', '5\nARV', '6\nARV',
'7\nARV', '8\nARV', '9\nARV', '10\nARV', '11\nARV', '12\nARV',
'13\nARV', '14\nARV', '15\nARV', '16\nARV', '17\nARV', '18\nARV',
'19\nARV', '20\nARV', '21\nARV', '22\nARV', '23\nARV', '24\nARV',
'25\nARV'], dtype=object)
#on crée un DF qui ne contienne que les classements d'arrivée
#on modifie les rangs 'X/nARV' en "X" pour les concurrents déjà arrivés
df_class_global_ARV = df_class_global.loc[df_class_global['Rang'].str.contains("ARV")].copy()
df_class_global_ARV['Rang'] = df_class_global_ARV['Rang'].str.extract(r"([0-9]*)")
#On passe ici le rang en "int"
df_class_global_ARV["Rang"] = df_class_global_ARV["Rang"].astype(int)
df_class_global_ARV["Rang"].unique()
array([ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17,
18, 19, 20, 21, 22, 23, 24, 25])
print("longueur totale : ", len(df_class_global_ARV))
print(df_class_global_ARV.isna().sum())
#On voit que plusieurs colonnes sont intégralement vides. On les supprime.
df_class_global_ARV = df_class_global_ARV.dropna(axis=1, how="all")
df_class_global_ARV.head()
longueur totale : 3931 Date 0 Heure 0 Rang 0 Nat 0 Voile 0 Skipper 0 Bateau 0 Heure FR 3931 Latitude 3931 Longitude 3931 Cap30 0 Vitesse30 0 VMG30 3931 Distance30 3931 CapLast 3931 VitesseLast 3931 VMGLast 220 DistanceLast 3931 Cap24 220 Vitesse24 0 VMG24 0 Distance24 0 DTF 0 DTL 0 dtype: int64
| Date | Heure | Rang | Nat | Voile | Skipper | Bateau | Cap30 | Vitesse30 | VMGLast | Cap24 | Vitesse24 | VMG24 | Distance24 | DTF | DTL | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 17834 | 2021-01-27 | 17:00:00 | 1 | FRA 79 | Charlie Dalin | APIVIA | 27/01/2021 21:35:47 FR | 80j 06h 15min 47s\n | NaN | NaN | 12.6 | 24355.0 | 119.6 % | 15.1 | 29135.0 | |
| 17873 | 2021-01-27 | 21:00:00 | 1 | FRA 17 | Yannick Bestaven | Maître Coq IV | 28/01/2021 05:19:46 FR | 80j 03h 44min 46s\n-10h 15min 00s | NaN | NaN | 12.6 | 24365.7 | 117.3 % | 14.8 | 28583.8 | |
| 17874 | 2021-01-27 | 21:00:00 | 2 | FRA 79 | Charlie Dalin | APIVIA | 27/01/2021 21:35:47 FR | 80j 06h 15min 47s\n | 02h 31min 01s | 02h 31min 01s | 12.6 | 24365.7 | 119.6 % | 15.1 | 29135.0 | |
| 17875 | 2021-01-27 | 21:00:00 | 3 | FRA 18 | Louis Burton | Bureau Vallée 2 | 28/01/2021 01:45:12 FR | 80j 10h 25min 12s\n | 06h 40min 26s | 04h 09min 25s | 12.6 | 24365.7 | 117.6 % | 14.8 | 28650.0 | |
| 17912 | 2021-01-28 | 04:00:00 | 1 | FRA 17 | Yannick Bestaven | Maître Coq IV | 28/01/2021 05:19:46 FR | 80j 03h 44min 46s\n-10h 15min 00s | NaN | NaN | 12.6 | 24365.7 | 117.3 % | 14.8 | 28583.8 |
#Le nom des colonnes ne correspond plus.
df_class_global_ARV.columns
df_class_global_ARV.columns = ['Date', 'Heure', 'Rang', 'Nat', 'Voile', 'Skipper', 'Bateau', "Date d'arrivée",
'Temps de course', 'EcartsToFirst', 'EcartsToPrevious', 'VitesseOrtho', 'DistanceOrtho','pourcentage', 'VitesseFond',
'DistanceFond']
df_class_global_ARV.head()
| Date | Heure | Rang | Nat | Voile | Skipper | Bateau | Date d'arrivée | Temps de course | EcartsToFirst | EcartsToPrevious | VitesseOrtho | DistanceOrtho | pourcentage | VitesseFond | DistanceFond | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 17834 | 2021-01-27 | 17:00:00 | 1 | FRA 79 | Charlie Dalin | APIVIA | 27/01/2021 21:35:47 FR | 80j 06h 15min 47s\n | NaN | NaN | 12.6 | 24355.0 | 119.6 % | 15.1 | 29135.0 | |
| 17873 | 2021-01-27 | 21:00:00 | 1 | FRA 17 | Yannick Bestaven | Maître Coq IV | 28/01/2021 05:19:46 FR | 80j 03h 44min 46s\n-10h 15min 00s | NaN | NaN | 12.6 | 24365.7 | 117.3 % | 14.8 | 28583.8 | |
| 17874 | 2021-01-27 | 21:00:00 | 2 | FRA 79 | Charlie Dalin | APIVIA | 27/01/2021 21:35:47 FR | 80j 06h 15min 47s\n | 02h 31min 01s | 02h 31min 01s | 12.6 | 24365.7 | 119.6 % | 15.1 | 29135.0 | |
| 17875 | 2021-01-27 | 21:00:00 | 3 | FRA 18 | Louis Burton | Bureau Vallée 2 | 28/01/2021 01:45:12 FR | 80j 10h 25min 12s\n | 06h 40min 26s | 04h 09min 25s | 12.6 | 24365.7 | 117.6 % | 14.8 | 28650.0 | |
| 17912 | 2021-01-28 | 04:00:00 | 1 | FRA 17 | Yannick Bestaven | Maître Coq IV | 28/01/2021 05:19:46 FR | 80j 03h 44min 46s\n-10h 15min 00s | NaN | NaN | 12.6 | 24365.7 | 117.3 % | 14.8 | 28583.8 |
df_class.to_csv('df_class_v3.csv', index=False)
df_class_global.to_csv('df_class_global_v3.csv', index=False)
df_class_global_ARV.to_csv('df_class_global_ARV_v3.csv', index=False)
On cherche à effectuer un "join" entre la table des classements et celle des informations sur les skippers. On compare donc leur allure générale et on vérifie donc qu'il existe une base fiable pour réaliser le join (le numéro de voile semblant le plus approprié).
df_class.head(2)
| Date | Heure | Rang | Nat | Voile | Skipper | Bateau | Heure FR | Latitude | Longitude | ... | CapLast | VitesseLast | VMGLast | DistanceLast | Cap24 | Vitesse24 | VMG24 | Distance24 | DTF | DTL | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 2020-11-08 | 14:00:00 | 1 | FRA 18 | Louis Burton | Bureau Vallée 2 | 15:30 | 46°24.46'N | 01°50.48'W | ... | 357 | 0.0 | 0.0 | 2788.0 | 201 | 0.3 | 0.3 | 6.1 | 24293.9 | 0.0 | |
| 1 | 2020-11-08 | 14:00:00 | 2 | MON 10 | Boris Herrmann | Seaexplorer - Yacht Club De Monaco | 15:31 | 46°24.34'N | 01°49.82'W | ... | 357 | 0.0 | 0.0 | 2787.9 | 196 | 0.3 | 0.2 | 6.0 | 24294.2 | 0.4 |
2 rows × 24 columns
df_class_global_ARV.head(2)
| Date | Heure | Rang | Nat | Voile | Skipper | Bateau | Date d'arrivée | Temps de course | EcartsToFirst | EcartsToPrevious | VitesseOrtho | DistanceOrtho | pourcentage | VitesseFond | DistanceFond | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 17834 | 2021-01-27 | 17:00:00 | 1 | FRA 79 | Charlie Dalin | APIVIA | 27/01/2021 21:35:47 FR | 80j 06h 15min 47s\n | NaN | NaN | 12.6 | 24355.0 | 119.6 % | 15.1 | 29135.0 | |
| 17873 | 2021-01-27 | 21:00:00 | 1 | FRA 17 | Yannick Bestaven | Maître Coq IV | 28/01/2021 05:19:46 FR | 80j 03h 44min 46s\n-10h 15min 00s | NaN | NaN | 12.6 | 24365.7 | 117.3 % | 14.8 | 28583.8 |
df_voiliers.head(2)
| Skipper | Numéro de voile | Anciens noms du bateau | Architecte | Chantier | Date de lancement | Longueur | Largeur | Tirant d'eau | Déplacement (poids) | Nombre de dérives | Hauteur mât | Voile quille | Surface de voiles au près | Surface de voiles au portant | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | Fabrice AMEDEO | FRA 56 | No Way Back, Vento di Sardegna | VPLP/Verdier | Persico Marine | 01 Août 2015 | 18,28 m | 5,85 m | 4,50 m | 7 t | foils | 29 m | monotype | 320 m2 | 570 m2 |
| 1 | Romain ATTANASIO | FRA 49 | Gitana Eighty, Synerciel, Newrest-Matmut | Bruce Farr Design | Southern Ocean Marine (Nouvelle Zélande) | 08 Mars 2007 | 18,28m | 5,80m | 4,50m | 9t | 2 | 28m | acier forgé | 280 m2 | 560 m2 |
#On passe tous les noms au même format (str.title())
df_voiliers['Skipper'] = df_voiliers['Skipper'].str.title()
print(df_class["Voile"].isna().sum())
print(df_class["Voile"].unique())
print(df_class["Voile"].nunique())
0 ['FRA 18' 'MON 10' 'FRA 8' 'FRA 59' 'FRA 53' 'FRA 56' 'FRA 01' 'GBR 99' 'FRA 69' 'GBR 777' 'FRA 85' 'FRA 92' 'FRA 49' 'FRA 1000' 'FRA 83' 'FRA 109' 'FRA 17' 'ITA 34' 'JPN 11' 'FRA 6' 'FRA 71' 'FRA 30' 'SUI 7' 'ESP 33' 'FRA 09' 'FRA 02' 'FRA 72' 'FRA 27' 'FRA 4' 'FRA 50' 'FIN 222' 'FRA 79' 'FRA 14'] 33
print(df_class_global_ARV["Voile"].isna().sum())
print(df_class_global_ARV["Voile"].unique())
print(df_class_global_ARV["Voile"].nunique())
0 ['FRA 79' 'FRA 17' 'FRA 18' 'FRA 59' 'MON 10' 'FRA 1000' 'ITA 34' 'FRA 01' 'FRA 09' 'FRA 53' 'FRA 02' 'FRA 30' 'FRA 8' 'FRA 49' 'FRA 14' 'JPN 11' 'SUI 7' 'FRA 92' 'GBR 777' 'ESP 33' 'FRA 83' 'FRA 50' 'FRA 71' 'FRA 72' 'FIN 222'] 25
print(df_voiliers["Numéro de voile"].isna().sum())
print(df_voiliers["Numéro de voile"].unique())
print(df_voiliers["Numéro de voile"].nunique())
1 ['FRA 56' 'FRA 49' 'FRA72' '17' '08' 'FRA 14' '18' 'ESP 33' 'FRA 71' 'FRA30' 'FRA 79' 'FRA109' '69' 'FRA09' 'FRA 85' 'FRA83' 'GBR77' '16' 'FIN222' 'FRA 27' '001' 'FRA 92' 'FRA50' 'ITA 34' 'SUI07' nan 'FRA1000' 'JPN 11' '4' 'FRA53' 'GBR 99' '2' 'FRA 6'] 32
#On cherche le skipper pour lequel on ignore le numéro de voile dans la table voiliers
nom = df_voiliers['Skipper'].loc[df_voiliers["Numéro de voile"].isna()].values[0]
nom
'Thomas Ruyant'
voile = df_class['Voile'].loc[df_class['Skipper'].str.contains(nom)].unique()[0]
voile
'FRA 59'
#On ajoute cette valeur à la place de la valeur manquante dans "df_voiliers"
df_voiliers["Numéro de voile"].loc[df_voiliers["Numéro de voile"].isna()] = voile
#On enlève les espaces pour toutes les valeurs de numéro de voile
df_voiliers["Numéro de voile"] = df_voiliers["Numéro de voile"].str.replace(" ", "")
df_class["Voile"] = df_class["Voile"].str.replace(" ", "")
df_class_global_ARV["Voile"] = df_class_global_ARV["Voile"].str.replace(" ", "")
#On compare ensuite les numéros de voile dans les deux datasets
voiles_c1 = set(df_class["Voile"].unique())
voiles_c2 = set(df_class_global_ARV["Voile"].unique())
voiles_v = set(df_voiliers["Numéro de voile"].unique())
print("voiles_c1 : ", voiles_c1, len(voiles_c1))
print("\nvoiles_c2 : ", voiles_c2, len(voiles_c2))
print("\nvoiles_v : ", voiles_v, len(voiles_v))
print("\n",voiles_c2 - voiles_c1) #Cohérence des fichiers de classement
print("\n",voiles_c1 - voiles_v)
print("\n",voiles_v - voiles_c1)
voiles_c1 : {'FRA109', 'FRA30', 'MON10', 'SUI7', 'FRA71', 'FRA09', 'FRA56', 'FRA53', 'FRA1000', 'FRA85', 'FRA92', 'FRA50', 'FRA83', 'ESP33', 'FRA59', 'FRA8', 'FRA69', 'FRA14', 'JPN11', 'ITA34', 'GBR99', 'FRA02', 'FRA18', 'FRA27', 'FRA6', 'FRA72', 'FRA17', 'GBR777', 'FRA01', 'FRA4', 'FRA49', 'FIN222', 'FRA79'} 33
voiles_c2 : {'FRA30', 'MON10', 'SUI7', 'FRA71', 'FRA09', 'FRA1000', 'FRA53', 'FRA92', 'FRA50', 'FRA83', 'ESP33', 'FRA59', 'FRA8', 'FRA14', 'JPN11', 'FRA02', 'ITA34', 'FRA18', 'FRA72', 'FRA17', 'FRA01', 'FRA49', 'GBR777', 'FIN222', 'FRA79'} 25
voiles_v : {'FRA109', '2', 'FRA30', 'SUI07', 'FRA71', '16', 'FRA09', 'FRA56', 'FRA1000', 'FRA53', 'FRA85', 'FRA92', 'FRA50', 'FRA83', 'ESP33', '001', 'FRA59', 'FRA14', '17', 'JPN11', 'GBR77', 'FRA27', 'ITA34', 'GBR99', 'FRA6', '4', '69', 'FRA72', 'FRA49', 'FIN222', '08', 'FRA79', '18'} 33
set()
{'FRA8', 'MON10', 'FRA69', 'FRA17', 'SUI7', 'FRA01', 'FRA4', 'GBR777', 'FRA02', 'FRA18'}
{'2', '08', '4', '69', 'SUI07', '17', '16', 'GBR77', '001', '18'}
#on ajoute les nationalités (pour df_voiliers) devant les nombres sans nationalité
# il s'agit à chaque fois de la France, sauf pour le numéro 16
pattern1 = r"^[0-9]*[^6]$"
repl1 = lambda m: "FRA"+m.group(0)
df_voiliers["Numéro de voile"] = df_voiliers["Numéro de voile"].str.replace(pattern1, repl1, regex=True)
#On ajoute un 0 systématique pour les numéros de voile à un chiffre (4 -> 04)
pattern2 = r"([A-Z])([0-9])$"
repl2 = lambda m: m.group(1)+"0"+m.group(2)
df_class["Voile"] = df_class["Voile"].str.replace(pattern2, repl2, regex=True)
df_class_global_ARV["Voile"] = df_class_global_ARV["Voile"].str.replace(pattern2, repl2, regex=True)
df_voiliers["Numéro de voile"] = df_voiliers["Numéro de voile"].str.replace(pattern2, repl2, regex=True)
#on en enlève un quand il y en a deux en début de nombre (ex : 001 -> 01)
pattern3 = r"([A-Z])0(0[0-9])$"
repl3 = lambda m: m.group(1)+m.group(2)
df_voiliers["Numéro de voile"] = df_voiliers["Numéro de voile"].str.replace(pattern3, repl3, regex=True)
voiles_c = set(df_class["Voile"].unique())
voiles_v = set(df_voiliers["Numéro de voile"].unique())
print(voiles_c - voiles_v)
print("\n",voiles_v - voiles_c)
{'GBR777', 'MON10'}
{'16', 'GBR77'}
df_voiliers.loc[df_voiliers["Numéro de voile"]=="GBR77"]
| Skipper | Numéro de voile | Anciens noms du bateau | Architecte | Chantier | Date de lancement | Longueur | Largeur | Tirant d'eau | Déplacement (poids) | Nombre de dérives | Hauteur mât | Voile quille | Surface de voiles au près | Surface de voiles au portant | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 16 | Pip Hare | GBR77 | Armor Lux, We Are Water, La Fabrique | Pierre Rolland | Bernard Stamm | 03 Juillet 1999 | 18,28 m | 5,70 m | 4,50 m | 9 t | 2 | 29 m | carbone | 300 m2 | 580 m2 |
#Sur les images du site, on voit que la voile est bien numérotée "GBR77".
#On corrige donc l'erreur dans l'autre dataset :
df_class["Voile"] = df_class["Voile"].str.replace("GBR777","GBR77")
df_class_global_ARV["Voile"] = df_class_global_ARV["Voile"].str.replace("GBR777","GBR77")
voiles_c = set(df_class["Voile"].unique())
voiles_v = set(df_voiliers["Numéro de voile"].unique())
print(voiles_c - voiles_v)
print("\n",voiles_v - voiles_c)
{'MON10'}
{'16'}
nom2 = df_voiliers["Skipper"].loc[df_voiliers["Numéro de voile"]=="16"].values[0]
voile2 = df_class['Voile'].loc[df_class['Skipper'].str.contains(nom2)].unique()[0]
voile2
'MON10'
#On constate que les deux noms de voile restants se réfèrent au
#même skipper. On n'en conserve qu'un.
df_voiliers["Numéro de voile"] = df_voiliers["Numéro de voile"].str.replace("16",voile2)
voiles_c = set(df_class["Voile"].unique())
voiles_v = set(df_voiliers["Numéro de voile"].unique())
print(voiles_c - voiles_v, voiles_v - voiles_c)
set() set()
#on corrige aussi les différences sur les noms des Skippers
sk_v = set(df_voiliers["Skipper"].unique())
sk_c = set(df_class["Skipper"].unique())
print(sk_v-sk_c)
sk_c-sk_v
{'Arnaud Boissières', 'Sam Davies', 'Alan Roura'}
{'Alan Roura', 'Arnaud Boissieres', 'Samantha Davies'}
df_voiliers["Skipper"] = df_voiliers["Skipper"].str.replace("Sam Davies","Samantha Davies")
df_voiliers["Skipper"] = df_voiliers["Skipper"].str.replace("Alan Roura","Alan Roura")
df_class["Skipper"]= df_class["Skipper"].str.replace("Arnaud Boissieres","Arnaud Boissières")
df_class_global["Skipper"]= df_class_global["Skipper"].str.replace("Arnaud Boissieres","Arnaud Boissières")
df_class_global_ARV["Skipper"]= df_class_global_ARV["Skipper"].str.replace("Arnaud Boissieres","Arnaud Boissières")
#On peut réaliser le "join" - un pour le tableau avant arrivée du 1er, l'autre après
df_voiliers.rename(columns={"Numéro de voile": "Voile"}, inplace=True)
#jusqu'au 27/1/21
df_join = df_class.join(df_voiliers.set_index(['Voile','Skipper']), on=['Voile',"Skipper"])
#à partir du 27/1/21
df_join_ARV = df_class_global_ARV.join(df_voiliers.set_index(['Voile','Skipper']), on=['Voile',"Skipper"])
df_join_ARV.tail(3)
| Date | Heure | Rang | Nat | Voile | Skipper | Bateau | Date d'arrivée | Temps de course | EcartsToFirst | ... | Date de lancement | Longueur | Largeur | Tirant d'eau | Déplacement (poids) | Nombre de dérives | Hauteur mât | Voile quille | Surface de voiles au près | Surface de voiles au portant | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 26397 | 2021-03-05 | 08:00:00 | 23 | FRA71 | Manuel Cousin | Groupe Sétin | 20/02/2021 08:35:40 FR | 103j 18h 15min 40s\n | 23j 14h 30min 54s | ... | 02 Février 2007 | 18,28 m | 5,80 m | 4,50 m | 9 t | 2 asymétriques | 28,50 | basculante sur vérin hydraulique | 270 m2 | 560 m2 | |
| 26398 | 2021-03-05 | 08:00:00 | 24 | FRA72 | Alexia Barrier | TSE - 4myplanet | 28/02/2021 07:23:44 FR | 111j 17h 03min 44s\n | 31j 13h 18min 58s | ... | 01 Mars 1998 | 18,28m | 5,54m | 4,50m | 9t | 2 | 29 m | acier | 260 m2 | 580 m2 | |
| 26399 | 2021-03-05 | 08:00:00 | 25 | FIN222 | Ari Huusela | Stark | 05/03/2021 08:35:46 FR | 116j 18h 15min 46s\n | 36j 14h 31min 00s | ... | 06 Août 2007 | 18,28 m | 5,80 m | 4,50 m | 8,5 t | 2 | 28 m | acier | 270 m2 | 580 m2 |
3 rows × 29 columns
skippers = df_voiliers["Skipper"].unique()
#on crée un dictionnaire qui à chaque nom de skipper, associe le dataframe avec tous ses résultats
skippers_df = {}
col_skip = ["Date","Heure", "Rang","Latitude", "Longitude", "DTF", "DTL", "VMG24"]
for i in range(len(skippers)):
df = df_class_global.loc[df_class_global['Skipper']==skippers[i]]
df = df.dropna(subset=["Longitude"]) #pour enlever les lignes après arrivée
skippers_df[skippers[i]]=df
df.to_csv('Skippers\\'+str(skippers[i])+'.csv', index=False)
skippers[2]
'Alexia Barrier'
skippers_df[skippers[2]].tail()
| Date | Heure | Rang | Nat | Voile | Skipper | Bateau | Heure FR | Latitude | Longitude | ... | CapLast | VitesseLast | VMGLast | DistanceLast | Cap24 | Vitesse24 | VMG24 | Distance24 | DTF | DTL | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 24996 | 2021-02-27 | 08:00:00 | 24 | FRA 72 | Alexia Barrier | TSE - 4myplanet | 08:30 | 46°25.53'N | 05°41.82'W | ... | 28 | 8.9 | 5.7 | 35.5 | 44 | 6.7 | 6.3 | 161.4 | 161.6 | 0.0 | |
| 25035 | 2021-02-27 | 11:00:00 | 24 | FRA 72 | Alexia Barrier | TSE - 4myplanet | 11:30 | 46°48.21'N | 05°22.20'W | ... | 31 | 8.8 | 4.8 | 26.4 | 45 | 6.7 | 6.2 | 161.1 | 149.0 | 0.0 | |
| 25074 | 2021-02-27 | 14:00:00 | 24 | FRA 72 | Alexia Barrier | TSE - 4myplanet | 14:30 | 47°14.68'N | 05°02.49'W | ... | 27 | 9.9 | 3.4 | 29.7 | 45 | 6.8 | 6.0 | 162.7 | 141.6 | 0.0 | |
| 25113 | 2021-02-27 | 17:00:00 | 24 | FRA 72 | Alexia Barrier | TSE - 4myplanet | 17:30 | 47°34.44'N | 04°44.42'W | ... | 32 | 7.8 | 1.8 | 23.3 | 47 | 6.6 | 5.7 | 159.2 | 138.0 | 0.0 | |
| 25152 | 2021-02-27 | 21:00:00 | 24 | FRA 72 | Alexia Barrier | TSE - 4myplanet | 21:30 | 47°20.51'N | 03°55.97'W | ... | 113 | 8.9 | 8.9 | 35.6 | 54 | 6.6 | 6.1 | 158.4 | 102.6 | 0.0 |
5 rows × 24 columns
On utilise ici le dataframe df_join_ARV. On effectue une régression linéaire pour expliquer le rang final de chaque voilier en fonction de la moyenne de toutes ses vitesses utiles depuis 24 heures.
#liste des participants ayant terminé la course
skippers_ARV = df_class_global_ARV["Skipper"].unique()
#liste des classements finaux et vitesses moyennes par participant ayant terminé la course
clas_VMG = []
for skip in skippers_ARV:
Vmean = np.mean(skippers_df[skip]["VMG24"].astype(float))
clasfinal = df_class_global_ARV.tail(len(skippers_ARV))
clas = clasfinal.loc[clasfinal["Skipper"]==skip]['Rang'].values[0]
clas_VMG.append((clas, Vmean))
clas_VMG2 = pd.DataFrame(clas_VMG) #données pour tous les concurrents finishers
clas_VMG_last = clas_VMG2.loc[clas_VMG2[0]>=10] #On regarde les 10 1ers concurrents
clas_VMG_first = clas_VMG2.loc[clas_VMG2[0]<=10] #on regarde les 15 derniers
y = clas_VMG2[0].to_numpy()
x = clas_VMG2[1].to_numpy()
y2 = clas_VMG_last[0].to_numpy()
x2 = clas_VMG_last[1].to_numpy()
y3 = clas_VMG_first[0].to_numpy()
x3 = clas_VMG_first[1].to_numpy()
"""reg = LinearRegression().fit(x.reshape(-1, 1),y)
reg2 = LinearRegression().fit(x.reshape(-1, 1),y)
reg3 = LinearRegression().fit(x.reshape(-1, 1),y)"""
reg = LinearRegression().fit(x.reshape(-1, 1),y)
reg2 = LinearRegression().fit(x2.reshape(-1, 1),y2)
reg3 = LinearRegression().fit(x3.reshape(-1, 1),y3)
plt.plot(x,y,'b.', label="Données réelles")
plt.plot(x, reg.intercept_ + x*reg.coef_, "r", label= "Régression Linéaire globale")
plt.plot(x2, reg2.intercept_ + x2*reg2.coef_, "g", label= "Régression Linéaire (15 derniers)")
plt.plot(x3, reg3.intercept_ + x3*reg3.coef_, "b", label= "Régression Linéaire3 (10 premiers)")
plt.legend()
plt.xlabel="Vitesse moyenne (kts)"
plt.ylabel="Classement final"
plt.title("Classement final des participants en fonction de leur vitesse moyenne");
De façon prévisible, on observe une très nette corrélation entre la vitesse moyenne des voiliers et leur classement. Toutefois, la dernière partie de la courbe est particulièrement intéressante : on constate que la vitesse des meilleurs skippers atteint un plateau autour de 12,5 kts, et ce n'est donc manifestement plus la vitesse qui a permis de départager les 9 premiers concurrents. Il s'agit sans doute plutôt de la trajectoire suivie.
Des irrégularités apparaissent (vitesse moyenne du 1er inférieure à celles du 2ème et 3ème), mais s'expliquent aussi par le fait que le classement ne dépende pas seulement de la date d'arrivée, mais aussi des bonus accordés aux skippers (par exemple pour le sauvetage de leurs concurrents en difficulté).
df_voiliers['Nombre de dérives'].unique()
array(['foils', '2', '2 asymétriques', 'foiler'], dtype=object)
On considère que "foiler" et "foils" désignent la même chose (des voiliers qui possèdent un/plusieurs foils).
#liste des classements finaux, vitesses moyennes, et présence de foil, pour tous les participants
foiler = ["foils","foiler"]
clas_VMG_foil = []
clasfinal = df_class_global_ARV.tail(len(skippers_ARV))
for skip in skippers:
if skip in skippers_ARV:
Vmean = np.mean(skippers_df[skip]["VMG24"].astype(float))
clas = clasfinal.loc[clasfinal["Skipper"]==skip]['Rang'].values[0]
if (df_voiliers.loc[df_voiliers['Skipper']==skip, "Nombre de dérives"].values[0] in foiler):
foil = 1
else: foil = 0
clas_VMG_foil.append((clas, Vmean, foil))
else:
Vmean = np.mean(skippers_df[skip]["VMG24"].astype(float))
clas = 27
if (df_voiliers.loc[df_voiliers['Skipper']==skip, "Nombre de dérives"].values[0] in foiler):
foil = 1
else: foil = 0
clas_VMG_foil.append((clas, Vmean, foil))
#données pour tous les concurrents
clas_VMG_foil2 = pd.DataFrame(clas_VMG_foil)
clas_VMG_foil2.columns=['Classement final',"Vitesse moyenne (kts)","Présence de foil"]
#Données pour les finishers uniquement
clas_VMG_foil_f = clas_VMG_foil2.loc[clas_VMG_foil2["Classement final"]!=27]
clas_VMG_foil2.head()
| Classement final | Vitesse moyenne (kts) | Présence de foil | |
|---|---|---|---|
| 0 | 27 | 8.594000 | 1 |
| 1 | 14 | 11.196673 | 0 |
| 2 | 24 | 9.077015 | 0 |
| 3 | 1 | 12.527329 | 1 |
| 4 | 13 | 11.228015 | 1 |
sns.scatterplot(x=clas_VMG_foil2["Vitesse moyenne (kts)"],
y=clas_VMG_foil2["Classement final"],
data=clas_VMG_foil2, hue=clas_VMG_foil2["Présence de foil"]);
Les voiliers équipés de foil semblent répartis dans tout le classement. Toutefois, si les finishers avec foil semblent meilleurs que leurs concurrents sans foil, les voiliers avec foil sont également sur-représentés parmi les abandons.
L'impact de cet équipement sur le classement n'est donc pas directement visible. En revanche, il semble avoir une incidence directe sur la vitesse.
On le vérifie toutefois par une régression linéaire (avec la présence ou non de foil exprimée en "one-hot vector").
#On normalise les données pour donner le même poids aux deux variables explicativses
#Tous les skippers - pour le scatterplot
x40 = StandardScaler().fit_transform(clas_VMG_foil2[["Vitesse moyenne (kts)","Présence de foil"]].values.reshape(-1,2))
x4 = StandardScaler().fit_transform(clas_VMG_foil2["Vitesse moyenne (kts)"].values.reshape(-1,1))
y4 = StandardScaler().fit_transform(clas_VMG_foil2["Présence de foil"].values.reshape(-1,1))
z4 = StandardScaler().fit_transform(clas_VMG_foil2["Classement final"].values.reshape(-1,1))
#Seulement les finisher - pour la régression linéaire
x50 = StandardScaler().fit_transform(clas_VMG_foil_f[["Vitesse moyenne (kts)","Présence de foil"]].values.reshape(-1,2))
x5 = StandardScaler().fit_transform(clas_VMG_foil_f["Vitesse moyenne (kts)"].values.reshape(-1,1))
y5 = StandardScaler().fit_transform(clas_VMG_foil_f["Présence de foil"].values.reshape(-1,1))
z5 = StandardScaler().fit_transform(clas_VMG_foil_f["Classement final"].values.reshape(-1,1))
x5_lin = np.linspace(min(x5),max(x5),100)
y5_lin = np.linspace(min(y5),max(y5),100)
z5_lin = np.linspace(min(z5),max(z5),100)
xx, yy = np.meshgrid(x5_lin, y5_lin)
reg5 = LinearRegression().fit(x50,z5)
a,b = reg5.coef_[0]
def regression(x_var,y_var):
return a*x_var + b*y_var
zz = regression(xx, yy)
#%matplotlib notebook
fig = plt.figure(figsize=(8,8))
ax = Axes3D(fig)
ax.scatter(x4, y4, z4, label='Données globales',c='b')
surface1 = ax.plot_surface(xx, yy, zz, color='purple', alpha=0.3, label='Régression linéaire (finishers)')
#Pour que les labels des plot_surface soient pris en compte
surface1._facecolors2d = surface1._facecolors3d
surface1._edgecolors2d = surface1._edgecolors3d
ax.set_title("Classement final des participants en fonction de leur vitesse moyenne et de la présence de foil")
ax.set_xlabel("Vitesse moyenne (kts)")
ax.set_ylabel("Présence de foil")
ax.set_zlabel("Classement final")
plt.legend()
ax.view_init(10,-40)
#ax.view_init(0,-90) #Pour voir l'impact de la vitesse sur le classement
#ax.view_init(-90,-90) #Pour voir l'impact du foil sur la vitesse
plt.show()
Sur le plan de régression, on observe une augmentation du classement quand un foil est présent(plan s'incline vers le haut pour foil = 1). (mode de visualisation n°1)
La relation entre le classement et la vitesse apparaît également très clairement. (mode de visualisation n°2)
En revanche, en ce qui concerne l'impact sur la vitesse, l'influence du foil n'est pas évidente, le plan de régression se trouvant quasiment dans le plan "horizontal". On l'observe toutefois plutôt bien sur les données réelles. (mode de visualisation n°)
#Liste des distances parcourues par chaque skipper, avec leur classement
clas_dist = []
nom_dist = []
#On récupère la somme des distances entre chaque classement
for skip in skippers:
dist = skippers_df[skip]['DistanceLast'].iloc[1:].astype(float).sum()
if skip in skippers_ARV:
clas = clasfinal.loc[clasfinal["Skipper"]==skip]['Rang'].values[0]
else:
clas = 27
clas_dist.append((clas, dist))
nom_dist.append((skip,dist))
clas_dist = pd.DataFrame(clas_dist)
nom_dist = pd.DataFrame(nom_dist).set_index(0)
fig = plt.figure(figsize=(10,5))
fig.subplots_adjust(hspace=0.5, wspace=0.5)
ax1 = plt.subplot(1,2,1)
ax1.hist(clas_dist[1],bins=100)
ax1.title.set_text("Distribution des distances parcourues")
ax2 = plt.subplot(1,2,2)
ax2.scatter(clas_dist[0], clas_dist[1], c="black", marker='.')
ax2.title.set_text("Distances parcourues en fonction du rang final");
nom_dist.sort_values(1).plot(kind='barh');
Ces distributions ne sont pas très intéressantes pour les finisher, car elles permettent surtout de voir que la distance totale de la course est d'environ 27.000 noeuds marins. En revanche, cela permet de voir à quelle "proportion" de la course ont eu lieu les abandons. Une difficulté (météorologique ou géographique ?) semble avoir poussé plusieurs coureurs à l'abandon autour de 8-9.000 nm (environ 1/3 du parcours).
Il serait dans tous les cas plus intéressant d'observer ces distances dans le temps.
nom = nom_dist.reset_index().set_index(1)
clas = clas_dist.set_index(1)
distances_temps = nom.join(clas,lsuffix="nom")
distances_temps.columns=["Nom","Classement"]
distances_temps = distances_temps.reset_index()[["Nom","Classement"]]
distances_temps = distances_temps.set_index("Nom").T
#On récupère la somme des distances entre chaque classement pour chaque journée
#On retire la première journée qui a une valeur absurde de la "distance depuis dernier classement"
dfs = []
for skip in skippers:
df = skippers_df[skip][["Date","Heure","DistanceLast"]].set_index(["Date","Heure"]).astype(float).iloc[1:]
df.columns=[skip]
df = df.cumsum()
dfs.append(df)
df_global = dfs[0]
for k in range(1,len(skippers)):
df_global = df_global.join(dfs[k], how="outer")
df_global.index = df_global.index.droplevel(1)
df_global = df_global.reset_index().drop_duplicates(subset=["Date"], keep="last").set_index("Date")
titre = "Distance parcourue par chaque skipper en fonction du temps"
df_global.plot(figsize=(10,10), title=titre).legend(bbox_to_anchor=(1.5, 1));
Ce graphique permet de bien visualiser la différence de vitesse entre les concurrents. On constate par exemple que l'un des abandons a eu lieu à mi-chemin après une importante perte de vitesse (courbe du bas qui se détache du "bloc"), mais les autres abandons sont dans le bloc, ces autres démissionnaires n'ont pas semblé perdre de vitesse, leur abandon est sans doute lié à d'autres raisons.
def dms2dd(s):
try:
degrees, minutes, direction = re.split('[°\'"]+', s)
dd = float(degrees) + float(minutes)/60
if direction in ('S','W'):
dd*= -1
return dd
except:
return s
skippers_df2={}
for skip in skippers:
skippers_df[skip]["Latitude"] = skippers_df[skip]["Latitude"].apply(dms2dd)
skippers_df[skip]["Latitude"] = skippers_df[skip]["Latitude"].apply(dms2dd)
skippers_df[skip]["Longitude"] = skippers_df[skip]["Longitude"].apply(dms2dd)
skippers_df[skip]["Longitude"] = skippers_df[skip]["Longitude"].apply(dms2dd)
geo = skippers_df[skippers[1]][["Latitude","Longitude"]]
skippers_df[skippers[0]]
| Date | Heure | Rang | Nat | Voile | Skipper | Bateau | Heure FR | Latitude | Longitude | ... | CapLast | VitesseLast | VMGLast | DistanceLast | Cap24 | Vitesse24 | VMG24 | Distance24 | DTF | DTL | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 5 | 2020-11-08 | 14:00:00 | 6 | FRA 56 | Fabrice Amedeo | Newrest - Art et Fenetres | 15:31 | 46.419833 | -1.831167 | ... | 357 | 0.0 | 0.0 | 2788.7 | 200 | 0.2 | 0.2 | 5.3 | 24294.5 | 0.7 | |
| 50 | 2020-11-08 | 15:00:00 | 14 | FRA 56 | Fabrice Amedeo | Newrest - Art et Fenetres | 17:00 | 46.359500 | -2.434000 | ... | 262 | 17.0 | 15.9 | 25.2 | 252 | 1.2 | 1.1 | 28.1 | 24271.1 | 6.0 | |
| 97 | 2020-11-08 | 17:00:00 | 24 | FRA 56 | Fabrice Amedeo | Newrest - Art et Fenetres | 18:30 | 46.402500 | -3.054167 | ... | 276 | 17.2 | 13.8 | 25.8 | 263 | 2.2 | 2.1 | 52.7 | 24250.9 | 16.0 | |
| 143 | 2020-11-08 | 21:00:00 | 33 | FRA 56 | Fabrice Amedeo | Newrest - Art et Fenetres | 22:30 | 46.463167 | -2.496500 | ... | 81 | 5.8 | -5.3 | 23.4 | 265 | 1.2 | 1.1 | 29.4 | 24272.2 | 93.3 | |
| 180 | 2020-11-09 | 04:00:00 | 33 | FRA 56 | Fabrice Amedeo | Newrest - Art et Fenetres | 05:30 | 46.488667 | -1.786833 | ... | 87 | 4.2 | -3.7 | 29.4 | 176 | 0.0 | 0.0 | 0.8 | 24298.2 | 165.1 | |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 7237 | 2020-12-10 | 21:00:00 | 23 | FRA 56 | Fabrice Amedeo | Newrest - Art et Fenetres | 21:30 | -39.828500 | 16.158500 | ... | 41 | 15.7 | 6.8 | 62.7 | 65 | 13.8 | 10.8 | 331.0 | 17639.1 | 3348.6 | |
| 7274 | 2020-12-11 | 04:00:00 | 23 | FRA 56 | Fabrice Amedeo | Newrest - Art et Fenetres | 04:30 | -38.800167 | 17.461333 | ... | 44 | 12.3 | 5.6 | 86.4 | 55 | 13.3 | 8.8 | 318.5 | 17600.9 | 3391.7 | |
| 7313 | 2020-12-11 | 08:00:00 | 25 | FRA 56 | Fabrice Amedeo | Newrest - Art et Fenetres | 08:30 | -38.097500 | 17.788833 | ... | 20 | 11.2 | 0.1 | 44.9 | 46 | 12.7 | 6.6 | 304.7 | 17600.9 | 3419.2 | |
| 7350 | 2020-12-11 | 11:00:00 | 25 | FRA 56 | Fabrice Amedeo | Newrest - Art et Fenetres | 11:30 | -37.671333 | 17.723667 | ... | 353 | 8.6 | -4.0 | 25.8 | 36 | 12.1 | 4.5 | 289.8 | 17613.1 | 3451.5 | |
| 7388 | 2020-12-11 | 14:00:00 | 26 | FRA 56 | Fabrice Amedeo | Newrest - Art et Fenetres | 14:30 | -37.163167 | 17.548833 | ... | 345 | 10.5 | -6.3 | 31.6 | 30 | 11.5 | 2.9 | 275.4 | 17632.4 | 3517.3 |
200 rows × 24 columns
geo = skippers_df[skippers[0]][["Latitude","Longitude"]].copy()
geo["Skipper"]=skippers[0]
for i in range(1,len(skippers)):
df = skippers_df[skippers[i]][["Latitude","Longitude"]].copy()
df.dropna(how="all", inplace=True)
df["Skipper"]=skippers[i]
geo = geo.append(df)
geo
| Latitude | Longitude | Skipper | |
|---|---|---|---|
| 5 | 46.419833 | -1.831167 | Fabrice Amedeo |
| 50 | 46.359500 | -2.434000 | Fabrice Amedeo |
| 97 | 46.402500 | -3.054167 | Fabrice Amedeo |
| 143 | 46.463167 | -2.496500 | Fabrice Amedeo |
| 180 | 46.488667 | -1.786833 | Fabrice Amedeo |
| ... | ... | ... | ... |
| 1673 | 22.944667 | -28.742833 | Nicolas Troussel |
| 1708 | 20.553167 | -28.898667 | Nicolas Troussel |
| 1746 | 19.437000 | -28.893333 | Nicolas Troussel |
| 1787 | 19.456333 | -28.927667 | Nicolas Troussel |
| 1824 | 19.386500 | -28.826000 | Nicolas Troussel |
15272 rows × 3 columns
import plotly.express as px
fig = px.line_geo(geo,
lat="Latitude",
lon="Longitude",
color="Skipper", # "continent is one of the columns of gapminder
projection="orthographic")
fig.show()
#image ne s'enregistre pas toujours dans le notebook, nécessité de ré-exécuter la cellule
#sans exécuter : copie d'écran insérée ci-dessous
On constate sur cette carte que les trajectoires sont assez distinctes d'un concurrent à l'autre, surtout au niveau de l'équateur dans l'Atlantique. Ces différences, sans doute liées aux courants et vents, permettent aussi de visualiser la différence de distance parcourue par les skippers, et pourrait expliquer l'écart à l'arrivée entre les concurrents.